本篇大綱:Generator、Component、Layout
截至目前,我們已經學會 D3 如何將資料與DOM 元素綁定來呈現資料視覺化,也知道要怎麼將資料整理成想要的內容,接下來我們就要看看 D3 如何建構圖形囉!
看到這邊大家心裡可能會有個疑問:不是用 SVG 去建構圖形就好嗎?為何 D3 還要設計其它建構圖形的API 呢?
那是因為我們之前提到 SVG 提供的內建幾何圖形(圓形、矩形、線條、路徑等)其實只是小小的集合,一張完整圖表則是由幾百個這些小元件組成的複雜集合,如果只使用 SVG 提供的圖形,就需要很辛苦地一個一個建立圖形,才能完成一張圖表。為了省去這個麻煩,D3 創建了很多不同的 API 來協助建構複雜圖形/圖表,這些 API 們也因此便被稱為 helper functions。
這些用來協助繪製圖型的 helper functions 可以依照使用的資料複雜度與產生出來的結果來劃分為三大類:
Generator
:產生 < path > 的d標籤路徑Component
:產生 DOM 元素Layout
:產生整張圖表我們先看到下面這張圖表
看完不太懂也沒關係,下面我們會分別講講這三大類 helper functions 的特性~
這一大類的 API 們是 D3 裡面最基本的方法~主要是透過使用基礎的資料集(像是array、number等等),來產生繪製 svg <path>需要的命令列字串(d)
。
我們在 Day3-SVG 那篇有提到,如果我想在 svg 使用 < path > 去繪製一條線的話,需要透過 d 屬性與屬性值去設定這條線的位置
<path
d="M50 20 C80 90,40 200,250,100" // <==就是這個傢伙
stroke="black"
fill="none"
stroke-width="2"
/>
但 d 屬性值的這一大串英文+數字基本上很難用人力去自行換算,因此我們就要借助 d3 的API去計算。
d3.line( )
我們先以 d3.line( ) 來示範:一樣先到官網看看 d3.line( ) 有哪些API可以使用
接著來看看 d3.line( )官方的解說,瞭解可以帶入那些參數、哪些方法必須搭配使用
從官網的解說得知,d3.line() 可以帶入兩個參數來進行運算:分別是 x 跟 y 值,而這兩個參數可以是數字或是方法
舉例來說,假設我們手上有一筆資料,想把它換算成 需要的 d 屬性值
// Line Generator
const data1 = [{x:10,y:10},{x:20,y:10},{x:30,y:10},{x:40,y:10},{x:50,y:10}]
一開始先用 line( ) 來設定方法
const line = d3.line()
.x(d=> d.x) // 設定x值要抓哪些資料
.y(d=> d.y) // 設定y值要抓哪些資料
設定好方法後,我們再將手上的這筆資料帶進去,就能得到想要的值了
line(data1) // 帶入要換算的資料,得到"M10,10L20,10L30,10L40,10L50,10"
取得可以用在d屬性上的值後,最後就是把這些資料綁訂到指定的 DOM 元素上面啦~
// html
<svg class="line"></svg>
// js
d3.select('.line')
.append('path')
.attr('d', line(data1))
.attr('stroke', 'black')
.attr('stroke-width', '2')
.attr('fill', 'none')
成功產出線條!
是不是很簡單呢? Generator 這類的方法主要就是在做這些事: 將資料換算成繪製 svg 需要的 code
。這一類常見的API 包含:line ( )、arc ( )、area( )、symbol ( )
等等,都是將資料換算並產出 < path > 的d屬性值,再將值套到 DOM 元素上去繪製圖型。我們再來多看幾個 Generator 的範例吧!
d3.area( )
一樣上[官網](https://github.com/d3/d3-shape/blob/v3.0.1/README.md#area)去看解說,得知d3.area( ) 有三個必須要帶的參數,分別是
API | 解釋 |
---|---|
area.x( ) | x的座標 |
area.y1( ) | y軸做邊 |
area.y0( ) | 開始繪製區域的y軸範圍 |
知道應該要帶那些參數後,我們就開始繪製區域圖案吧!我們手上有的資料是 data1,一樣使用 d3.area( ) 先設定 area 這個方法
// html
<svg class="area"></svg>
// js
const data1 = [{x:10,y:100},{x:20,y:100},{x:30,y:100},{x:90,y:20},{x:220,y:10}]
const area = d3.area()
.x(d=>d.x)
.y1(d=>d.y)
.y0(10)
area(data1) // 呼叫方法並帶入資料,得到 M10,100L20,100L30,100L90,20L220,10L220,10L90,10L30,10L20,10L10,10Z
得到 d 的屬性值後,我們就可以將這個資料帶進選定的 DOM 元素啦
d3.select('.area')
.append('path')
.attr('d', area(data1))
.attr('stroke', 'blue')
.attr('fill', 'blue')
登登登~成功得到一個填滿區域的圖型!
這邊只是基礎的介紹跟使用,等之後與 scale( )、axis( ) 等其他方法結合後,就可以使用 area( ) 去繪製出類似下方的圖表,是不是很好看呀~
d3.arc( )
最後再來看到另一個 Generator 中很用到的API — d3.arc( ),這個 API 主要是用來畫弧線,它通常會跟 pie( ) 這個 API 結合繪製圓餅圖。但它還能搭配其他API 畫另外一種很酷炫的圖,猜得到是什麼圖表嗎?
就是「車子的儀錶板」!
是不是很酷呀?很想知道要怎麼畫嗎?先別急,我們先來看看要怎麼使用 d3.arc( )。一樣先看到官方文件,得知要使用 d3.arc( ) 需要搭配另外四個 API
參數 | 解釋 |
---|---|
arc.innerRadius( ) | 內圈範圍 |
arc.outerRadius( ) | 外圈範圍 |
arc.startAngle( ) | 起始角度 |
arc.endAngle( ) | 終點角度 |
官方文件上對於 arc( ) 跟它的定義也寫得很清楚
了解這些後,我們就可以直接來使用 arc( ) 啦!
<svg class="arc"></svg>
// js
const arc = d3.arc()
.innerRadius(40) // 內圈範圍40
.outerRadius(50) // 內圈範圍50
.startAngle(0)
.endAngle(Math.PI*0.5) // 畫一個 1/4 的圓形
d3.select('.arc')
.append("g")
.attr("transform", "translate(100,100)") // 把整個圓弧移動到 100,100 的位置
.append('path')
.attr('d', arc())
.attr('stroke', 'blue')
.attr('fill', 'blue')
這樣就能得到 1/4 個半圓弧啦~
因此,如果想要畫出方向盤的圓弧,只要調整一下 start angle 跟 end angle;想改變圓的大小則是調整 innerRadius 跟 outerRadius
// arc
const arc = d3.arc()
.innerRadius(60)
.outerRadius(65)
.startAngle(Math.PI*1.2)
.endAngle(Math.PI*2.8)
d3.select('.arc')
.append("g")
.attr("transform", "translate(150,80)")
.append('path')
.attr('d', arc())
.attr('stroke', 'blue')
.attr('fill', 'blue')
接下來我們來講講第二類 helper functions — Components。上面提到 Generators 只建立給 < path > 用的d 的命令指令,而 Components 則完全相反。這一大類的 API 們會使用回傳的方法去建立一整組圖形物件,供給特定的圖表使用
。就拿這一大類中最被使用的 d3.axis( )
來舉例好了,.axis( ) 會接收 scale( ) 回傳的方法,接著繪製出 < line >, < path >, < g > 與 < text > 等等的一堆元素,一起組成一組軸線。我們實際來看看例子會更清楚:
axis ( )
一樣先開官方文件來看看有哪些 API 可以用!
知道有這些API 可以使用之後,我們來看看 axis( ) 的解說~官方文件上其實就是簡單短短的一行
axis( ) 用來將 scale( )的資料轉換成人類能看得懂的文字,讓這個最無趣的任務變得輕鬆簡單
沒錯,就是這麼簡單!因為當我們想使用 d3 建構圖表時,要先用 scale( ) 把數據資料換算成符合比例、能夠讓d3讀懂的數值,才能把這些數值傳換成圖表;而axis( ) 唯一的任務就是把 scale( ) 換算好的值,再轉成人類看得懂的文字,最後繪製成軸線。
這邊我們就來實際繪製一條X軸線看看吧:
// 我目前的資料集
const data1 = [{x:10,y:100},{x:20,y:100},{x:30,y:100},{x:90,y:20},{x:220,y:10}]
// 抓出 x 軸要使用的值
const xData = data1.map((i) => i.x);
// 設定X軸的比例尺與繪製範圍
const xScale = d3.scaleLinear()
.domain([0, d3.max(xData)])
.range([10, 290]);
//使用xScale的設定,繪製刻度(ticks)朝下的軸線
const xAxis = d3
.axisBottom(xScale)
// 呼叫軸線
d3.select('.axis').append("g").call(xAxis);
透過這幾個步驟,我們就能畫出一條寫有刻度的軸線啦!
看到這邊你可能會大喊:寫錯了!X軸應該在下面才對!賠錢、還我時間!!
先別激動,這是因為我們之前說過,svg 的原點都在左上角
,依序由上至下、由左至右建構圖形,所以這邊的X軸會飄在上方也是合情合理、完全沒錯。那要怎麼讓它導邪歸正,做回一條正常的X軸線呢?一樣就要派 transform 上場啦~
d3.select('.axis')
.append("g")
.call(xAxis)
.attr("transform", "translate(0,130)") // 調整X軸位置
透過修改 DOM 元素的 transform 值,我們就能將 X 軸移動到任何想要的位置上~
這邊由於篇幅的關係,axis( ) 就先介紹到這邊,等到後面專門講軸線的章節時,會有更詳細的解說。
看完 axis( ) 的例子後,有清楚 Components 這類的 API 在做什麼了嗎? 除了 axis( ) 之外,brush( )、zoom( ) 也都歸納在 Components 這一類 Helper Functions 內。後面也會有專門的章節來講這兩個 API,有興趣的人可以訂閱按讚開啟小鈴鐺
看完前面兩類 Helper Functions 之後,我們來看看最後一大類 Helper Functions 吧!Layouts 比起 Generator、Components 又更進階,這類的 API 是直接拿 一整個完整的資料集去繪製完整的圖表
,這類的 API 可以很直覺簡單,例如:pie( ) 繪製的圓餅圖;也可以很複雜,例如:force( ) 繪製的原力關聯圖。
Layouts 需要的完整資料集有可能是多個陣列,也有可能是 Generators 類的 API 產生的資料,它會使用資料集去計算像素座標與角度,常見的API 有 stack( )、pie( )、force( ),下面我們就用 stack( ) 來實際演練看看吧!
stack( )
stack( ) 這個 API 主要用來畫長條堆集圖,但還得要搭配 scale( )、axis( ) 等API 才能畫出下面的圖表。等到後面章節講完其他必要的API後,會再帶大家實際繪製圖表,今天就先稍微了解 stack( ) 在幹嘛就好
我們先看到 官網 上列出 stacks 有哪些 API 可以使用,以及它們的功能是什麼
接著我們來看看 d3.stack( )的解釋
文件上寫著當我們使用 d3.stack( ) 帶入一個資料陣列時,這個 API 會返還另一個代表每筆資料集的陣列,而這些資料集是用 keys 去決定的。看完後是不是覺得有看沒有懂呢?沒關係很正常(看得懂的是神人),我們來一一解說吧!
由於d3.stack( ) 大多被用來繪製長條堆積圖,因此很重要的一點就是:要把哪些資料歸為同一集合?假設我目前有一系列的資料,紀錄2021年1~4月,中國、美國、台灣的每月肺炎確診人數
const dataStack = [
{month: new Date(2021, 0, 1), China: 32, America: 20, Taiwan: 30},
{month: new Date(2021, 1, 1), China: 7, America: 27, Taiwan: 18},
{month: new Date(2021, 2, 1), China: 13, America: 33, Taiwan: 18},
{month: new Date(2021, 3, 1), China: 6, America: 18, Taiwan: 20}
];
目前的資料是:一個陣列內含四個物件,這四個物件內分別有 month、China、America、Taiwan 四個 key 值,而這些正是 d3.stack() 所需要的 keys。
當我們使用d3.stack() 時,它會根據資料的 key 值把資料分類,同樣 key 值的數據會被視為同一個集合,因此這邊就有四個集合
以這個例子來說明,我們先使用 d3.stack( ) 定義一個叫做 stack 的方法,設定要建立堆疊圖的資料 keys 分別是"China"、 "America"、 "Taiwan",接著再設定一個變數stackedSeries,這個變數是把 stack 方法帶 dataStack 的資料後得到的數值
const stack = d3.stack()
.keys(["China", "America", "Taiwan"]) // 設定資料的keys
const stackedSeries = stack(dataStack); // 把資料帶入stack方法
因為我們設定的keys有三項,因此 d3.stack() 會把同一個 key 的數值視為同一集合(series),就像這樣
接著使用這些集合去計算並各自返還一個陣列
每個集合中有幾筆資料,就會一一返還對應的陣列
我們把返還的陣列展開,看看裡面到底是放什麼資料~展開後我們會看到每個陣列都包含三筆資料,分別代表
這樣一來我們就得到需要的資料了,可以運用這些起始值跟終點值來繪製堆集圖啦!
// stack
const dataStack = [
{month: new Date(2021, 0, 1), China: 132, America: 80, Taiwan: 30},
{month: new Date(2021, 1, 1), China: 67, America: 27, Taiwan: 188},
{month: new Date(2021, 2, 1), China: 123, America: 153, Taiwan: 18},
{month: new Date(2021, 3, 1), China: 27, America: 112, Taiwan: 20}
];
const stack = d3.stack()
.keys(['China', 'America', 'Taiwan'])
const stackedSeries = stack(dataStack);
console.log(stackedSeries)
// 顏色
const colorScale = d3.scaleOrdinal()
.domain(['China', 'America', 'Taiwan'])
.range(["red", "blue", "orange"])
// 建立集合元素g、設定顏色
const g = d3.select('.stack')
.attr('width', 300)
.selectAll('g')
.data(stackedSeries)
.enter()
.append('g')
.attr('fill', d => colorScale(d.key));
// 繪製長條圖
g.selectAll('rect')
.data(d=>d)
.join('rect')
.attr('width', d => d[1] - d[0]) // 長度為終點值減掉起始值
.attr('x', d => d[0]) // x 座標設定為起始值
.attr('y', (d, i) => i *30) // y 座標用 index 來處理,乘上每條bar想拉開的距離
.attr('height', 20);
我們先把不同的 keys 所組合的資料集設定不同顏色,接著使用 d3.stack( ) 返還的起始值跟終點值去設定每條bar中,不同資料集的起迄位置,最後用 < rect > 來繪製橫向長條圖,就完成啦!!!
其實了解 d3 API 的運作原理、需要的資料以及返還什麼東西之後,是不是覺得圖表也沒有很難畫了呀~雖然 Layout 這一類的 API 相比 Generators 跟 Components 更難懂一些,但只要搞懂這些 API 返還什麼值、要怎麼運用,其實就能輕鬆畫出想要的圖表了。
今天的 Helper Functions 就講到這邊~看完之後相信大家對於 d3 用來繪製形狀的 API 都有一定的認識啦,明天開始要進入 d3 動畫跟互動的部分囉!敬請期待!
最後的最後,一樣附上本章的程式碼與圖表 Github 、 Github Page,需要的人請自行取用~
看完後是不是覺得有看沒有懂呢?沒關係很正常(看得懂的是神人)
所以金金是神人
我也是看了各種神人的解說跟實際做一次才懂的XD 光靠文件的描述真的是無法~~